#include "traverse.h"

/**
 * build a long directory name
 * precondition:
 *   parent_dir is string without trailing slash (/)
 *   so is dir
 * returns:
 *   a new string with slash inserted between parent_dir and dir
 *   the new string is dynamically allocated
 *   NOTE: it is the caller's responsibility to free the memory
 */
char *build_long_name(const char *parent_dir, const char *dir)
{
    char *long_name = malloc(strlen(parent_dir) + strlen(dir) + 2);

    if(long_name) {
        strcpy(long_name, parent_dir);
        strcat(long_name, "/");
        strcat(long_name, dir);
    }

    return long_name;
}

typedef int (*_stat)(const char *, struct stat *);

/**
 * traverse directory, without guaranteed order
 * precondition:
 *   dir is a string without trailing slash (/)
 *   pred is NOT NULL
 * also:
 *   depth indicates current depth
 *   min_depth indicates the minimum depth to get traversed
 *   max_depth indicates the maximun depth to get traversed
 *   min_depth should be no larger than max_depth
 */
void _traverse(char *dir, predicate *pred, action _action, int depth, int min_depth, int max_depth, _stat leveln_stat)
{
    if(depth > max_depth)
        return;

    DIR *dp;
    struct dirent *entry;
    struct stat statbuf;

    if((dp = opendir(dir)) == NULL) {
        fprintf(stderr, "ERROR: cannot open directory: %s\n", dir);
        return;
    }

    chdir(dir);
    while((entry = readdir(dp)) != NULL) {
        /* ignore . and .. */
        if(strcmp(".", entry->d_name) == 0 ||
                strcmp("..", entry->d_name) == 0)
            continue;

        if(depth >= min_depth && pred->test(pred, entry)) {
            /* do specified action */
            _action(dir, entry);
        }

        char *full_name = build_long_name(dir, entry->d_name);
        if(leveln_stat(full_name, &statbuf)) {
            /* something goes wrong */
            free(full_name);
            continue;
        }

        /* if entry is directory */
        if(S_ISDIR(statbuf.st_mode)) {
            /* printf("INFO: %s is directory\n", long_name); */
            /* Recurse at a new indent level */
            _traverse(full_name, pred, _action, depth+1, min_depth, max_depth, leveln_stat);
        }
        free(full_name);

        memset(&statbuf, 0, sizeof(statbuf));
    }
    chdir("..");
    closedir(dp);
}

/**
 * Traverse directory, wrapper.
 * This function determines whether the file is directory or not.
 * If so, a recursive call is made.
 */
void traverse(char *dir, predicate *pred, action _action, int min_depth, int max_depth, enum SYMLINK_OPTION symlink_option)
{
    int depth = 0; /* current depth */
    if(depth > max_depth)
        return;

    _stat level0_stat = lstat;
    _stat leveln_stat = lstat;
    switch(symlink_option) {
        case NO_FOLLOW: break;
        case FOLLOW: level0_stat = stat; leveln_stat = stat; break;
        case ONLY_LEVEL0: level0_stat = stat; break;
    }

    struct stat statbuf;
    if(level0_stat(dir, &statbuf)) {
        error_exit(1, "cannot stat file %s", dir);
        return;
    }

    /* faking a dirent */
    struct dirent *entry = (struct dirent *)malloc(sizeof(struct dirent));
    char *dup1 = strdup(dir); /* duplacate string to avoid side effect */
    char *dup2 = strdup(dir); /* of dirname and basename */
    char *dir_name = dirname(dup1);
    char *base_name = basename(dup2);
    strncpy(entry->d_name, base_name, 256);
    if(S_ISREG(statbuf.st_mode)) {
        entry->d_type = DT_REG;
    } else if(S_ISDIR(statbuf.st_mode)){
        entry->d_type = DT_DIR;
    } else if(S_ISCHR(statbuf.st_mode)){
        entry->d_type = DT_CHR;
    } else if(S_ISBLK(statbuf.st_mode)){
        entry->d_type = DT_BLK;
    } else if(S_ISFIFO(statbuf.st_mode)){
        entry->d_type = DT_FIFO;
    } else if(S_ISLNK(statbuf.st_mode)){
        entry->d_type = DT_LNK;
    } else if(S_ISSOCK(statbuf.st_mode)){
        entry->d_type = DT_SOCK;
    } else {
        entry->d_type = DT_UNKNOWN;
    }

    chdir(dir_name);
    if(depth >= min_depth && pred->test(pred, entry)) {
        /* do specified action */
        _action(dir_name, entry);
    }

    /* if is directory, traverse it */
    if(DT_DIR == entry->d_type)
        _traverse(dir, pred, _action, depth+1, min_depth, max_depth, leveln_stat);

    free(dup2);
    free(dup1);
    free(entry);
}

